package main

import (
	"bytes"
	"fmt"
	"runtime"
	"sort"
)

type test struct {
	name string
	f    func()
}

type stats struct {
	mallocs, frees float64
}

func (s stats) String() string {
	str := fmt.Sprintf("mallocs=%v", s.mallocs)
	if s.frees > 0 {
		str += fmt.Sprintf(" frees=%v", s.frees)
	}
	return str
}

func (t *test) time() stats {
	var t0, t1 runtime.MemStats
	runtime.ReadMemStats(&t0)
	const n = 100
	for i := 0; i < n; i++ {
		t.f()
	}
	runtime.ReadMemStats(&t1)
	return stats{
		mallocs: float64(t1.Mallocs-t0.Mallocs) / n,
		frees:   float64(t1.Frees-t0.Frees) / n,
	}
}

var tests = []test{
	{"Defer", func() {
		defer func() {
		}()
	}},

	{"DeferClosure", func() {
		a := 0
		defer func() {
			_ = a
			a = 1
			_ = a
		}()
	}},

	{"RangeString", func() {
		a := "foo"
		for _, c := range a {
			_ = c
		}
	}},

	{"RangeStringBytes", func() {
		a := "foo"
		for _, c := range []byte(a) {
			_ = c
		}
	}},

	{"NonClosureFunc", func() {
		foo := func() {}
		foo()
	}},

	// Converting a string to a []byte slice could avoid allocating
	// if static analysis says that the byte slice is never written to.
	{"StringToReadonlyByteSlice", func() {
		onlyReadsBytes := func(bs []byte) {
			var sum byte
			for _, b := range bs {
				sum += b
			}
			_ = sum
		}
		s := "some string"
		onlyReadsBytes([]byte(s))
	}},

	// buf doesn't escape, so should remain on stack.
	{"LocalByteBuffer", func() {
		var buf bytes.Buffer
		buf.WriteString("foo")
		_ = buf.String()
	}},

	{"LocalByteBufferFmt", func() {
		var buf bytes.Buffer
		fmt.Fprintf(&buf, "foo")
		_ = buf.String()
	}},

	// Same as LocalByteBufferFmt, but using a format pattern.
	{"LocalByteBufferFmtS", func() {
		var buf bytes.Buffer
		fmt.Fprintf(&buf, "%s", "foo")
		_ = buf.String()
	}},

	{"StringAppend", func() {
		f := "foo"
		b := "bar"
		_ = f + b
	}},

	// new() inside a function which never escapes.
	{"NewNoEscapeLocal", func() {
		type foo struct{ x, y, z int64 }
		f := new(foo)
		f.x = 1
		f.y = 2
		_ = f
	}},

	// new() passed to an inlined function.
	{"NewNoEscapeInlined", func() {
		t := new(T)
		t.A = 10
		_ = noEscapeSimple(t)
	}},

	// new() passed to a chain of not-inlined functions.
	{"NewNoEscapeNotInlined", func() {
		t := new(T)
		t.A = 10
		_ = noEscapePass(t)
	}},

	// Looking up a string map key using a []byte.
	// This affects memcache, which has map keys in []byte, and needs
	// to look up things in a string-keyed hash map.
	{"MapLookupByteSlice", func() {
		_, ok := mapStrStr[string(someBytes)]
		if ok {
			panic("x")
		}
	}},

	// Passing a string to a function which only reads bytes from []byte.
	// The compiler could pass a byte slice sharing the memory of the string,
	// since lenByteSlice never mutates the []byte.
	{"StringToReadonlyByteFunc", func() {
		s := "foo string"
		lenByteSlice([]byte(s))
	}},

	{"BytesToReadonlyByteFunc", func() {
		lenByteSlice(someBytes)
	}},
}

func lenByteSlice(readOnlyBytes []byte) (n int) {
	for _ = range readOnlyBytes {
		n++
	}
	return
}

func Null() {}

type T struct {
	A, B int64
}

func noEscapeSimple(t *T) int64 {
	return t.A
}

func noEscapePass(t *T) int64 {
	return noopInt64(t.B)
}

func noopInt64(v int64) int64 {
	sort.Strings(nil) // prevent negative from being inlined.
	return v
}

var someBytes = []byte("some bytes")

var mapStrStr = map[string]string{
	"foo": "bar",
}

func main() {
	for _, t := range tests {
		st := t.time()
		fmt.Printf("%30s: %s\n", t.name, st)
	}
}
